/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.persistence.hibernate.manager;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map.Entry;

import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.proxy.HibernateProxyHelper;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateObjectRetrievalFailureException;
import org.springframework.transaction.annotation.Transactional;

import com.genia.toolbox.basics.bean.IterableIterator;
import com.genia.toolbox.persistence.bean.Persistable;
import com.genia.toolbox.persistence.bean.QueryParameters;
import com.genia.toolbox.persistence.criteria.Criteria;
import com.genia.toolbox.persistence.criteria.CriteriaResult;
import com.genia.toolbox.persistence.exception.ObjectIsNotPersistentException;
import com.genia.toolbox.persistence.exception.ObjectIsPersistentException;
import com.genia.toolbox.persistence.exception.PersistenceException;
import com.genia.toolbox.persistence.hibernate.bean.impl.HibernateResultIterableIterator;
import com.genia.toolbox.persistence.hibernate.criteria.HibernateCriteria;
import com.genia.toolbox.persistence.hibernate.criteria.HibernateCriteriaResult;

/**
 * The transactional part of {@link HibernatePersistenceManager}. This class
 * will be proxied to allow the transactions to be handled.
 */
public class TransactionalHibernatePersistenceManager implements IHibernatePersistenceManager
{

  /**
   * the {@link HibernatePersistenceManager} to use.
   */
  private HibernatePersistenceManager hibernatePersistenceManager;



  /**
   * This method deletes an object.
   * 
   * @param object
   *          The object to delete
   * @throws PersistenceException
   *           when an error occurred.
   */
  @SuppressWarnings("unchecked")
  @Transactional(rollbackFor = { PersistenceException.class })
  public void delete(final Persistable<? extends Serializable> object)
      throws PersistenceException
  {
    final Persistable objectToDelete = getHibernatePersistenceManager().get(HibernateProxyHelper.getClassWithoutInitializingProxy(object), object.getIdentifier());
    if (objectToDelete == null) {
      return;
    }
    try {
      getHibernatePersistenceManager().getHibernateTemplate().delete(objectToDelete);
    }
    catch (Exception e) {
      throw getHibernatePersistenceManager().convertException(e);
    }
  }



  /**
   * This method returns the results of a criteria.
   * 
   * @param <MAINTYPE>
   *          the main type of the criteria.
   * @param criteria
   *          the criteria that describes the query
   * @param parameters
   *          the parameters of this search
   * @return the list of <code>CriteriaResult</code> that represents the
   *         result of the query
   * @throws PersistenceException
   *           when the is a problem with the persisence layer
   */
  @SuppressWarnings("unchecked")
  @Transactional(readOnly = true)
  public <MAINTYPE> List<CriteriaResult<MAINTYPE>> find(final Criteria<MAINTYPE> criteria, final QueryParameters parameters)
      throws PersistenceException
  {
    final HibernateCriteria<MAINTYPE> hibernateCriteria = (HibernateCriteria<MAINTYPE>) criteria;
    try {
      return (List<CriteriaResult<MAINTYPE>>) getHibernatePersistenceManager().getHibernateTemplate().execute(new HibernateCallback()
      {

        public List<CriteriaResult<MAINTYPE>> doInHibernate(final Session session)
        {
          final List<CriteriaResult<MAINTYPE>> res = new ArrayList<CriteriaResult<MAINTYPE>>();
          for (final Object line : computeQuery(session, hibernateCriteria, parameters).list()) {
            if (hibernateCriteria.isJoined()) {
              res.add(new HibernateCriteriaResult<MAINTYPE>(hibernateCriteria, (Object[]) line));
            }
            else {
              res.add(new HibernateCriteriaResult<MAINTYPE>(hibernateCriteria, line));
            }
          }
          return res;
        }

      });
    }
    catch (final DataAccessException exception) {
      throw new PersistenceException(exception);
    }
  }



  /**
   * This method get an persited object given its class and identifier.
   * 
   * @param <IDENTIFIER>
   *          The type of the identifier
   * @param <TYPE>
   *          The type of the object to retrieve
   * @param type
   *          The class of the object to retrieve
   * @param identifier
   *          The identifier of the object to retrieve
   * @return the object of class <code>type</code> with the identifier
   *         <code>identifier</code> or <code>null</code> if no such object
   *         exists
   */
  @SuppressWarnings("unchecked")
  @Transactional(readOnly = true)
  public <IDENTIFIER extends Serializable, TYPE extends Persistable<IDENTIFIER>> TYPE get(final Class<TYPE> type, final IDENTIFIER identifier)
  {
    try {
      return (TYPE) getHibernatePersistenceManager().getHibernateTemplate().get(type, identifier);
    }
    catch (HibernateObjectRetrievalFailureException e) {
      if (e.getCause() != null && e.getCause() instanceof ObjectNotFoundException) {
        return null;
      }
      throw e;
    }
  }



  /**
   * getter for the hibernatePersistenceManager property.
   * 
   * @return the hibernatePersistenceManager
   */
  public HibernatePersistenceManager getHibernatePersistenceManager()
  {
    return hibernatePersistenceManager;
  }



  /**
   * This method iterates over the results of a criteria.
   * 
   * @param <MAINTYPE>
   *          the main type of the criteria
   * @param criteria
   *          the criteria that describes the query
   * @param parameters
   *          the parameters of this search
   * @return the list of <code>CriteriaResult</code> that represents the
   *         result of the query
   * @throws PersistenceException
   *           when the is a problem with the persisence layer
   * @see com.genia.toolbox.persistence.basis.manager.AbstractPersistenceManager#iterate(com.genia.toolbox.persistence.criteria.Criteria,
   *      com.genia.toolbox.persistence.bean.QueryParameters)
   */
  @SuppressWarnings("unchecked")
  @Transactional(readOnly = true)
  public <MAINTYPE> IterableIterator<CriteriaResult<MAINTYPE>> iterate(final Criteria<MAINTYPE> criteria, final QueryParameters parameters)
      throws PersistenceException
  {
    final HibernateCriteria<MAINTYPE> hibernateCriteria = (HibernateCriteria<MAINTYPE>) criteria;
    try {
      return (IterableIterator<CriteriaResult<MAINTYPE>>) getHibernatePersistenceManager().getHibernateTemplate().execute(new HibernateCallback()
      {

        public IterableIterator<CriteriaResult<MAINTYPE>> doInHibernate(final Session session)
        {
          return new HibernateResultIterableIterator<MAINTYPE>(hibernateCriteria, computeQuery(session, hibernateCriteria, parameters).scroll());
        }

      });
    }
    catch (final DataAccessException exception) {
      throw new PersistenceException(exception);
    }
  }



  /**
   * This method saves a new object.
   * 
   * @param object
   *          The object to save
   * @throws PersistenceException
   *           when an error occurred
   */
  @SuppressWarnings("unchecked")
  @Transactional(rollbackFor = { PersistenceException.class }, noRollbackFor = { ObjectIsPersistentException.class })
  public void save(final Persistable<? extends Serializable> object)
      throws PersistenceException
  {
    if (object.getIdentifier() != null && getHibernatePersistenceManager().get(HibernateProxyHelper.getClassWithoutInitializingProxy(object), object.getIdentifier()) != null) {
      throw new ObjectIsPersistentException();
    }
    try {
      getHibernatePersistenceManager().getHibernateTemplate().save(object);
    }
    catch (final Exception e) {
      throw getHibernatePersistenceManager().convertException(e);
    }

  }



  /**
   * This method saves a new object or update an already persistent object. This
   * method should be avoided as much as possible. Finding if an object is
   * persistent or not is tedious and error prone, unexpected comportment can
   * arrise.
   * 
   * @param object
   *          The object to save or update
   * @throws PersistenceException
   *           if any problems occur during persistence process
   * @see com.genia.toolbox.persistence.manager.PersistenceManager#saveOrUpdate(com.genia.toolbox.persistence.bean.Persistable)
   */
  @SuppressWarnings("unchecked")
  @Transactional(rollbackFor = { PersistenceException.class }, noRollbackFor = { ObjectIsPersistentException.class })
  public void saveOrUpdate(Persistable<? extends Serializable> object)
      throws PersistenceException
  {
    if (object.getIdentifier() != null && getHibernatePersistenceManager().get(HibernateProxyHelper.getClassWithoutInitializingProxy(object), object.getIdentifier()) != null) {
      update(object);
    }
    else {
      save(object);
    }
  }



  /**
   * setter for the hibernatePersistenceManager property.
   * 
   * @param hibernatePersistenceManager
   *          the hibernatePersistenceManager to set
   */
  public void setHibernatePersistenceManager(HibernatePersistenceManager hibernatePersistenceManager)
  {
    this.hibernatePersistenceManager = hibernatePersistenceManager;
  }



  /**
   * This method updates an object.
   * 
   * @param object
   *          The object to update
   * @throws PersistenceException
   *           when an error occurred
   */
  @SuppressWarnings("unchecked")
  @Transactional(rollbackFor = { PersistenceException.class }, noRollbackFor = { ObjectIsNotPersistentException.class })
  public void update(final Persistable<? extends Serializable> object)
      throws PersistenceException
  {
    try {
      final Persistable objectToUpdate = getHibernatePersistenceManager().get(HibernateProxyHelper.getClassWithoutInitializingProxy(object), object.getIdentifier());
      if (objectToUpdate == null) {
        throw new ObjectIsNotPersistentException();
      }
      getHibernatePersistenceManager().getHibernateTemplate().merge(object);
      getHibernatePersistenceManager().getHibernateTemplate().update(objectToUpdate);
    }
    catch (final Exception e) {
      throw getHibernatePersistenceManager().convertException(e);
    }
  }



  /**
   * this methods bind a parameter to its alias in the hibernate
   * <code>Query</code>.
   * 
   * @param query
   *          the <code>Query</code> in which to bind the parameters
   * @param parameter
   *          the parameter associated to its alias
   */
  private void bindParameter(final Query query, final Entry<String, Object> parameter) // NOPMD
  {
    final Object value = parameter.getValue();
    final String key = parameter.getKey();
    if (value instanceof Byte) {
      query.setByte(key, (Byte) value);
    }
    else if (value instanceof Short || value instanceof Integer || value instanceof Long) {
      query.setLong(key, ((Number) value).longValue());
    }
    else if (value instanceof Float || value instanceof Double) {
      query.setDouble(key, ((Number) value).doubleValue());
    }
    else if (value instanceof Boolean) {
      query.setBoolean(key, (Boolean) value);
    }
    else if (value instanceof Character) {
      query.setCharacter(key, (Character) value);
    }
    else if (value instanceof Collection) {
      query.setParameterList(key, (Collection<?>) value);
    }
    else if (value instanceof Date) {
      query.setTimestamp(key, (Date) value);
    }
    else if (value instanceof String) {
      query.setString(key, (String) value);
    }
    else if (value instanceof Persistable) {
      query.setEntity(key, value);
    }
    else {
      query.setParameter(key, value);
    }
  }



  /**
   * compute the {@link Query} associated to a criteria.
   * 
   * @param session
   *          the hibernate {@link Session} to use to generate the {@link Query}
   * @param hibernateCriteria
   *          the criteria generating the {@link Query}
   * @param parameters
   *          the parameters of this search
   * @return the {@link Query} associated to the criteria
   */
  private Query computeQuery(final Session session, final HibernateCriteria<?> hibernateCriteria, final QueryParameters parameters)
  {
    final String queryString = hibernateCriteria.getQuery();
    final Query query = session.createQuery(queryString);
    if (parameters.getFirstResult() != null) {
      query.setFirstResult(parameters.getFirstResult());
    }
    if (parameters.getMaxResults() != null) {
      query.setMaxResults(parameters.getMaxResults());
    }
    for (final Entry<String, Object> entry : hibernateCriteria.getParameters().entrySet()) {
      bindParameter(query, entry);
    }
    return query;
  }

}
